﻿using AIStudio.ConSole.Redis.Core;
using CSRedis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AIStudio.ConSole.Redis.Ch01
{
    /************************************************************************************************************************
    # <start id="upvote-code"/>
    ONE_WEEK_IN_SECONDS = 7 * 86400                     #A
    VOTE_SCORE = 432                                    #A

    def article_vote(conn, user, article):
        cutoff = time.time() - ONE_WEEK_IN_SECONDS      #B
        if conn.zscore('time:', article) < cutoff:      #C
            return

        article_id = article.partition(':')[-1]         #D
        if conn.sadd('voted:' + article_id, user):      #E
            conn.zincrby('score:', VOTE_SCORE, article) #E
            conn.hincrby(article, 'votes', 1)           #E
    # <end id="upvote-code"/>
    #A Prepare our constants
    #B Calculate the cutoff time for voting
    #C Check to see if the article can still be voted on (we could use the article HASH here, but scores are returned as floats so we don't have to cast it)
    #D Get the id portion from the article:id identifier
    #E If the user hasn't voted for this article before, increment the article score and vote count (note that our HINCRBY and ZINCRBY calls should be in a Redis transaction, but we don't introduce them until chapter 3 and 4, so ignore that for now)
    #END

    # <start id="post-article-code"/>
    def post_article(conn, user, title, link):
        article_id = str(conn.incr('article:'))     #A

        voted = 'voted:' + article_id
        conn.sadd(voted, user)                      #B
        conn.expire(voted, ONE_WEEK_IN_SECONDS)     #B

        now = time.time()
        article = 'article:' + article_id
        conn.hmset(article, {                       #C
            'title': title,                         #C
            'link': link,                           #C
            'poster': user,                         #C
            'time': now,                            #C
            'votes': 1,                             #C
        })                                          #C

        conn.zadd('score:', {article: now + VOTE_SCORE})#D
        conn.zadd('time:', {article: now})              #D

        return article_id
    # <end id="post-article-code"/>
    #A Generate a new article id
    #B Start with the posting user having voted for the article, and set the article voting information to automatically expire in a week (we discuss expiration in chapter 3)
    #C Create the article hash
    #D Add the article to the time and score ordered zsets
    #END

    # <start id="fetch-articles-code"/>
    ARTICLES_PER_PAGE = 25

    def get_articles(conn, page, order='score:'):
        start = (page-1) * ARTICLES_PER_PAGE            #A
        end = start + ARTICLES_PER_PAGE - 1             #A

        ids = conn.zrevrange(order, start, end)         #B
        articles = []
        for id in ids:                                  #C
            article_data = conn.hgetall(id)             #C
            article_data['id'] = id                     #C
            articles.append(article_data)               #C

        return articles
    # <end id="fetch-articles-code"/>
    #A Set up the start and end indexes for fetching the articles
    #B Fetch the article ids
    #C Get the article information from the list of article ids
    #END

    # <start id="add-remove-groups"/>
    def add_remove_groups(conn, article_id, to_add=[], to_remove=[]):
        article = 'article:' + article_id           #A
        for group in to_add:
            conn.sadd('group:' + group, article)    #B
        for group in to_remove:
            conn.srem('group:' + group, article)    #C
    # <end id="add-remove-groups"/>
    #A Construct the article information like we did in post_article
    #B Add the article to groups that it should be a part of
    #C Remove the article from groups that it should be removed from
    #END

    # <start id="fetch-articles-group"/>
    def get_group_articles(conn, group, page, order='score:'):
        key = order + group                                     #A
        if not conn.exists(key):                                #B
            conn.zinterstore(key,                               #C
                ['group:' + group, order],                      #C
                aggregate='max',                                #C
            )
            conn.expire(key, 60)                                #D
        return get_articles(conn, page, key)                    #E
    # <end id="fetch-articles-group"/>
    #A Create a key for each group and each sort order
    #B If we haven't sorted these articles recently, we should sort them
    #C Actually sort the articles in the group based on score or recency
    #D Tell Redis to automatically expire the ZSET in 60 seconds
    #E Call our earlier get_articles() function to handle pagination and article data fetching
    #END

    #--------------- Below this line are helpers to test the code ----------------

    class TestCh01(unittest.TestCase):
        def setUp(self):
            import redis
            self.conn = redis.Redis(db=15)

        def tearDown(self):
            del self.conn
            print()
            print()

        def test_article_functionality(self):
            conn = self.conn
            import pprint

            article_id = str(post_article(conn, 'username', 'A title', 'http://www.google.com'))
            print("We posted a new article with id:", article_id)
            print()
            self.assertTrue(article_id)

            print("Its HASH looks like:")
            r = conn.hgetall('article:' + article_id)
            print(r)
            print()
            self.assertTrue(r)

            article_vote(conn, 'other_user', 'article:' + article_id)
            print("We voted for the article, it now has votes:", end=' ')
            v = int(conn.hget('article:' + article_id, 'votes'))
            print(v)
            print()
            self.assertTrue(v > 1)

            print("The currently highest-scoring articles are:")
            articles = get_articles(conn, 1)
            pprint.pprint(articles)
            print()

            self.assertTrue(len(articles) >= 1)

            add_remove_groups(conn, article_id, ['new-group'])
            print("We added the article to a new group, other articles include:")
            articles = get_group_articles(conn, 'new-group', 1)
            pprint.pprint(articles)
            print()
            self.assertTrue(len(articles) >= 1)

            to_del = (
                conn.keys('time:*') + conn.keys('voted:*') + conn.keys('score:*') + 
                conn.keys('article:*') + conn.keys('group:*')
            )
            if to_del:
                conn.delete(*to_del)

    if __name__ == '__main__':
        unittest.main()

    **************************************************************************************************************/

    class TestCh01
    {
        public const int ONE_WEEK_IN_SECONDS = 7 * 86400;
        public const int VOTE_SCORE = 432;
        public const int ARTICLES_PER_PAGE = 25;

        CSRedisClient _conn;
        public TestCh01()
        {
            _conn = new CSRedis.CSRedisClient("127.0.0.1:6379,defaultDatabase=14,poolsize=500,ssl=false,writeBuffer=10240");
        }

        public void article_vote(CSRedisClient conn, string user, string article)
        {
            var cutoff = (DateTime.Now.AddSeconds(0 - ONE_WEEK_IN_SECONDS) - new DateTime(1970, 1, 1)).TotalSeconds;
            if ((conn.ZScore("time:", article) ?? 0) < (decimal)cutoff)
            {
                return;
            }

            var article_id = article.Substring(article.IndexOf(":") + 1);
            if (conn.SAdd("voted:" + article_id, user) > 0)
            {
                conn.ZIncrBy("score:", article, VOTE_SCORE);
                conn.HIncrBy(article, "votes", 1);
            }
        }

        public string post_article(CSRedisClient conn, string user, string title, string link)
        {
            var article_id = conn.IncrBy("article:").ToString();
            var voted = "voted:" + article_id;
            conn.SAdd(voted, user);
            conn.Expire(voted, ONE_WEEK_IN_SECONDS);
            var now = (DateTime.Now - new DateTime(1970, 1, 1)).TotalSeconds;
            var article = "article:" + article_id;
            conn.HMSet(article, new[]
            {
                "title", title,
                "link",link,
                "poster", user ,
                "time", now.ToString(),
                "votes", 1.ToString(),
            });

            conn.ZAdd("score:", ((decimal)(now + VOTE_SCORE), article));
            conn.ZAdd("time:", ((decimal)now, article));

            return article_id;
        }

        public List<Article> get_articles(CSRedisClient conn, int page, string order = "score:")
        {
            var start = (page - 1) * ARTICLES_PER_PAGE;
            var end = start + ARTICLES_PER_PAGE - 1;

            var ids = conn.ZRevRange(order, start, end);
            List<Article> articles = new List<Article>();
            foreach (var id in ids)
            {
                var article_data = conn.HGetAll(id);
                articles.Add(new Article() { id = id, link = article_data["link"], poster = article_data["poster"], time = article_data["time"], title = article_data["title"], votes = article_data["votes"], });
            }

            return articles;
        }

        public void add_remove_groups(CSRedisClient conn, string article_id, string[] to_add, string[] to_remove)
        {
            var article = "article:" + article_id;
            foreach (var group in to_add)
            {
                conn.SAdd("group:" + group, article);
            }
            foreach (var group in to_remove)
            {
                conn.SRem("group:" + group, article);
            }
        }

        public List<Article> get_group_articles(CSRedisClient conn, string group, int page, string order = "score:")
        {
            var key = order + group;
            if (!conn.Exists(key))
            {
                conn.ZInterStore(key, null, RedisAggregate.Max, "group:" + group, order);
                conn.Expire(key, 60);
            }

            return get_articles(conn, page, key);
        }

        public void test_article_functionality()
        {
            var conn = _conn;
            var article_id = post_article(conn, "username", "A title", "http://www.google.com");
            Console.WriteLine("We posted a new article with id:" + article_id);
            Console.WriteLine();

            Console.WriteLine("Its HASH looks like:");
            var r = conn.HGetAll("article:" + article_id);
            Console.WriteLine(PrintHelper.Dictionary2String(r));
            Console.WriteLine();

            article_vote(conn, "other_user", "article:" + article_id);
            Console.WriteLine("We voted for the article, it now has votes:");
            var v = conn.HGet("article:" + article_id, "votes");
            Console.WriteLine(v);
            Console.WriteLine();

            Console.WriteLine("The currently highest-scoring articles are:");
            var articles = get_articles(conn, 1);
            Console.WriteLine(PrintHelper.StringArray2String(articles.Select(p => p.ToString()).ToArray()));
            Console.WriteLine();

            add_remove_groups(conn, article_id, new string[] { "new-group" }, new string[] { });
            Console.WriteLine("We added the article to a new group, other articles include:");
            articles = get_group_articles(conn, "new-group", 1);
            Console.WriteLine(PrintHelper.StringArray2String(articles.Select(p => p.ToString()).ToArray()));
            Console.WriteLine();

            var to_del = conn.Keys("time:*").Union(conn.Keys("voted:*")).Union(conn.Keys("score:*")).Union(conn.Keys("article:*")).Union(conn.Keys("group:*")).ToArray();
            conn.Del(to_del);


        }
    }

    public class Article
    {
        public string id { get; set; }
        public string title { get; set; }
        public string link { get; set; }
        public string poster { get; set; }
        public string time { get; set; }
        public string votes { get; set; }

        public override string ToString()
        {
            return $"{id}-{title}-{link}-{poster}-{time}-{votes}";
        }
    }
}
